/*
 * License GNU LGPL
 * Copyright (C) 2012 Amrullah <amrullah@panemu.com>.
 */
package com.abc.cheque.ui.table;

import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Control;
import javafx.scene.control.TableCell;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

/**
 *
 * @author Amrullah <amrullah@panemu.com>
 */
public abstract class BaseCell<S, T> extends TableCell<S, T> {

    private Control control;

    public BaseCell() {
        contentDisplayProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                if (control == null) {
                    control = getEditor();
                    attachEnterEscapeEventHandler();
                    attachFocusListener();
                    setGraphic(control);
                }
                setValueToEditor(getItem());
            }
        });

        /**
         * commit change on lost focus
         */
        this.selectedProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                if (isEditing()) {
                    commitEdit(getValueFromEditor());
                }
            }
        });

    }

    private void attachFocusListener() {
        /**
         * Set cell mode to edit if the editor control receives focus. This is
         * intended to deal with mouse click. This way, commitEdit() will be
         * called if the cell is no longer focused
         */
        control.focusedProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                if (!BaseCell.this.isSelected() && newValue) {
                    getTableView().getSelectionModel().select(getIndex(), getTableColumn());
                }
                if (!isEditing() && newValue) {
                    programmaticallyEdited = true;
                    getTableView().edit(getIndex(), getTableColumn());
                    programmaticallyEdited = false;
                }
            }
        });
    }
    private boolean programmaticallyEdited = false;

    @Override
    public void startEdit() {
        /**
         * If a row is added, new cells are created. The old cells are not
         * disposed automatically. They still respond to user event's.
         * Fortunately, the "should-be-discarded" cells have invisible row so we
         * can recognize them and prevent them to interact with user's event.
         */
        if (!this.getTableRow().isVisible()) {
            return;
        }
        super.startEdit();
        if (!programmaticallyEdited) {

            if (control == null) {
                control = getEditor();
                attachEnterEscapeEventHandler();
                attachFocusListener();
            }
            setGraphic(control);
            setValueToEditor(getItem());
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
            /**
             * put focus on the textfield so user can directly typed on it
             */
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    control.requestFocus();
                }
            };
            Platform.runLater(r);
        }
    }

    protected abstract void setValueToEditor(T value);

    protected abstract T getValueFromEditor();

    protected abstract Control getEditor();

    @Override
    public void commitEdit(T newValue) {
        super.commitEdit(newValue);
        updateItem(newValue, false);
    }

    @Override
    public void cancelEdit() {
        setValueToEditor(getItem());
        super.cancelEdit();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                getTableView().requestFocus();
            }
        };
        Platform.runLater(r);
    }

    @Override
    public void updateItem(T item, boolean empty) {
        boolean emptyRow = getTableView().getItems().size() < getIndex() + 1;
        /**
         * don't call super.updateItem() because it will trigger cancelEdit() if
         * the cell is being edited. It causes calling commitEdit() ALWAYS call
         * cancelEdit as well which is undesired.
         *
         */
        if (!isEditing()) {
            super.updateItem(item, empty && emptyRow);
        }
        if (empty && isSelected()) {
            updateSelected(false);
        }
        if (empty && emptyRow) {
            setText(null);
            //do not nullify graphic here. Let the TableRow to control cell dislay
            //setGraphic(null);
        } else {
            if (isEditing()) {
//                setGraphic(getEditor());
//                    setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
            } else {
                setText(getString(item));
            }
        }
    }

    protected String getString(T value) {
        return getItem() == null ? "" : getItem().toString();
    }

    protected void attachEnterEscapeEventHandler() {
        control.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent t) {
                if (t.getCode() == KeyCode.ENTER) {
                    commitEdit(getValueFromEditor());
                } else if (t.getCode() == KeyCode.ESCAPE) {
                    cancelEdit();
                }
            }
        });
    }
}
